/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "openvpn_fd.h"

#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/uio.h>
#include <stdatomic.h>
#include <stdarg.h>
#include "memdbg.h"
#include "error.h"
#include <errno.h>

const int CONNECT_TIMEOUT = 1;
const int INVALID_FD = -1;
const int NETMANAGER_EXT_ERR_INTERNAL = 2200003;
const int NETMANAGER_EXT_SUCCESS = 0;

atomic_int tunFd_ = 0;

int ConnectControl(int sockfd, int nsec)
{
    uint32_t flags = (uint32_t)fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    struct sockaddr_un serv_addr;
    memset((char *) &serv_addr, 0, sizeof(serv_addr));
    serv_addr.sun_family = AF_UNIX;
    strcpy(serv_addr.sun_path, "/dev/unix/socket/tunfd");

    /* EINPROGRESS - Indicates that the connection establishment has been started but is not complete */
    int ret = connect(sockfd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr));
    if (ret < 0 && (errno != EINPROGRESS)) {
        msg(M_ERR, "connect error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else if (ret == 0) {
        /* connect completed immediately, This can happen when the server is on the client's host*/
        fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
        msg(M_INFO, "connect success.");
        return NETMANAGER_EXT_SUCCESS;
    }

    fd_set rset;
    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    fd_set wset = rset;

    struct timeval tval;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;
    ret = select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL);
    if (ret < 0) { // select error.
        msg(M_ERR, "select error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else if (ret == 0) { // timeout
        msg(M_ERR, "connect timeout.");
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else { // fd ready
        int result = -1;
        socklen_t len = sizeof(result);
        if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
            if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &result, &len) < 0) {
                msg(M_ERR, "getsockopt error: %d", errno);
                return NETMANAGER_EXT_ERR_INTERNAL;
            }
        } else {
            msg(M_ERR, "select error: sockfd not set");
            return NETMANAGER_EXT_ERR_INTERNAL;
        }

        if (result != NETMANAGER_EXT_SUCCESS) {
            msg(M_ERR, "connect failed. error: %d", result);
            return NETMANAGER_EXT_ERR_INTERNAL;
        } else {
            fcntl(sockfd, F_SETFL, flags);
            msg(M_INFO, "connect success.");
            return NETMANAGER_EXT_SUCCESS ;
        }
    }
}

int RecvMsgFromUnixServer(int sockfd)
{
    char buf[1] = {0};
    struct iovec iov = {
        .iov_base = buf,
        .iov_len = sizeof(buf),
    };
    union {
        struct cmsghdr align;
        char cmsg[CMSG_SPACE(sizeof(int))];
    } cmsgu;
    memset(cmsgu.cmsg, 0, sizeof(cmsgu.cmsg));
    struct msghdr message;
    memset(&message,  0, sizeof(message));
    message.msg_iov = &iov;
    message.msg_iovlen = 1;
    message.msg_control = cmsgu.cmsg;
    message.msg_controllen = sizeof(cmsgu.cmsg);
    if (recvmsg(sockfd, &message, 0) < 0) {
        msg(M_ERR, "recvmsg msg error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    }

    struct cmsghdr *cmsgh = CMSG_FIRSTHDR(&message);
    if (cmsgh == NULL) {
        msg(M_ERR, "cmsgh is nullptr");
        return NETMANAGER_EXT_ERR_INTERNAL;
    }
    if (cmsgh->cmsg_level != SOL_SOCKET || cmsgh->cmsg_type != SCM_RIGHTS ||
        cmsgh->cmsg_len != CMSG_LEN(sizeof(int))) {
        msg(M_ERR, "cmsg_level: %d, cmsg_type: %d, cmsg_len: %d", cmsgh->cmsg_level, cmsgh->cmsg_type, cmsgh->cmsg_len);
        return NETMANAGER_EXT_ERR_INTERNAL;
    }

    memcpy(&tunFd_, CMSG_DATA(cmsgh), sizeof(tunFd_));
    return NETMANAGER_EXT_SUCCESS;
}

int GetVpnInterfaceFd()
{
    CloseVpnInterfaceFd();
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0) {
        msg(M_ERR, "create unix SOCK_STREAM socket error: %d", errno);
        return INVALID_FD;
    }

    if (ConnectControl(sockfd, CONNECT_TIMEOUT) != NETMANAGER_EXT_SUCCESS){
        close(sockfd);
        msg(M_ERR, "connect error: %d", errno);
        return INVALID_FD;
    }

    if (RecvMsgFromUnixServer(sockfd) != NETMANAGER_EXT_SUCCESS){
        close(sockfd);
        return INVALID_FD;
    }

    close(sockfd);
    msg(M_INFO, "receive tun device fd: %d", atomic_load(&tunFd_));
    return tunFd_;
}

void CloseVpnInterfaceFd()
{
    if (tunFd_ > 0) {
        msg(M_INFO, "close tunfd %d of vpn interface", atomic_load(&tunFd_));
        close(tunFd_);
        tunFd_ = 0;
    }
}
